﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Text;
using TimScripts.BF4Rcon.Frostbite;

namespace TimScripts.BF4Rcon
{
    public class RconEventManager
    {
        private const string SquadPropertyName = "squadId";
        private Dictionary<IRconEventProvider, ISynchronousRconClient> Clients = new Dictionary<IRconEventProvider, ISynchronousRconClient>();
        private Dictionary<string, Action<IRconEventProvider, IList<string>>> EventHandlers = new Dictionary<string, Action<IRconEventProvider, IList<string>>>(StringComparer.OrdinalIgnoreCase);

        public event EventHandler<PlayerEventArgs> PlayerAuthenticated;
        public event EventHandler<PlayerEventArgs> PlayerLeave;
        public event EventHandler<PlayerEventArgs> PlayerSpawn;
        public event EventHandler<PlayerKillEventArgs> PlayerKill;
        public event EventHandler<PlayerChatEventArgs> PlayerChat;
        public event EventHandler<PunkBusterMessageEventArgs> PunkBusterMessage;
        public event EventHandler<MaxPlayerCountChangeEventArgs> MaxPlayerCountChange;
        public event EventHandler<LevelLoadEventArgs> LevelLoad;
        public event EventHandler<RoundOverEventArgs> RoundOver;

        private Dictionary<IRconEventProvider, RoundOverState> RoundOverData = new Dictionary<IRconEventProvider, RoundOverState>();



        public void AddEventProvider(IRconEventProvider eventProvider)
        {
            if (eventProvider == null)
                throw new ArgumentNullException("eventProvider");

            eventProvider.RconEvent += RconEventHandler;

            RoundOverData[eventProvider] = new RoundOverState();
        }

        public void AddEventProvider(IRconEventProvider eventProvider, ISynchronousRconClient client)
        {
            if (eventProvider == null)
                throw new ArgumentNullException("eventProvider");

            if (client == null)
                throw new ArgumentNullException("client");

            Clients[eventProvider] = client;

            AddEventProvider(eventProvider);
        }

        public void RemoveEventProvider(IRconEventProvider eventProvider)
        {
            if (eventProvider == null)
                throw new ArgumentNullException("eventProvider");

            Clients.Remove(eventProvider);

            eventProvider.RconEvent -= RconEventHandler;

            RoundOverData.Remove(eventProvider);
        }

        public RconEventManager()
        {
            CreateEventHandlers();
        }

        #region Event Handlers
        [RconEvent("player.onAuthenticated")]
        private void PlayerAuthenticatedEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            Player p = GetPlayer(eventProvider, data[0]);
            PlayerEventArgs args = new PlayerEventArgs(p);

            RaiseEvent(eventProvider, PlayerAuthenticated, args);
        }

        [RconEvent("player.onLeave")]
        private void PlayerLeaveEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            PlayerInfoBlockPlayerCollection playerData = new PlayerInfoBlockPlayerCollection(data, 1);
            Player p = playerData[0];
            PlayerEventArgs args = new PlayerEventArgs(p);

            RaiseEvent(eventProvider, PlayerLeave, args);
        }

        [RconEvent("player.onSpawn")]
        private void PlayerSpawnEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            Player p = GetPlayer(eventProvider, data[0], SquadPropertyName, data[1]);
            PlayerEventArgs args = new PlayerEventArgs(p);

            RaiseEvent(eventProvider, PlayerSpawn, args);
        }

        [RconEvent("player.onKill")]
        private void PlayerKillEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            Player killer = GetPlayer(eventProvider, data[0]);
            Player victim = GetPlayer(eventProvider, data[1]);

            string weapon = data[2];
            bool isHeadshot = bool.Parse(data[3]);

            PlayerKillEventArgs args = new PlayerKillEventArgs(killer, victim, weapon, isHeadshot);

            RaiseEvent(eventProvider, PlayerKill, args);
        }

        [RconEvent("player.onChat")]
        private void PlayerChatEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            Player player = GetPlayer(eventProvider, data[0]);
            string message = data[1];
            PlayerSubset target = ParsePlayerSubset(eventProvider, data, 2);
            PlayerChatEventArgs args = new PlayerChatEventArgs(player, message, target);

            RaiseEvent(eventProvider, PlayerChat, args);
        }

        //TODO: team and squad change

        //TODO: an event that is raised when unknown

        [RconEvent("punkBuster.onMessage")]
        private void PunkBusterMessageEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            PunkBusterMessageEventArgs args = new PunkBusterMessageEventArgs(data[0]);

            RaiseEvent(eventProvider, PunkBusterMessage, args);
        }

        [RconEvent("server.onMaxPlayerCountChange")]
        private void nMaxPlayerCountChangeEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            MaxPlayerCountChangeEventArgs args = new MaxPlayerCountChangeEventArgs(data[0].ToInt());

            RaiseEvent(eventProvider, MaxPlayerCountChange, args);
        }

        [RconEvent("server.onLevelLoaded")]
        private void LevelLoadEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            Map map = Map.Parse(data[0], data[1]);
            int roundsPlayed = data[2].ToInt();
            int totalRounds = data[3].ToInt();
            LevelLoadEventArgs args = new LevelLoadEventArgs(map, roundsPlayed, totalRounds);

            RaiseEvent(eventProvider, LevelLoad, args);
        }

        [RconEvent("server.onRoundOver")]
        private void RoundOverEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            int winningTeam = data[0].ToInt();
            RoundOverState state = RoundOverData[eventProvider];

            state.TeamId = winningTeam;

            RaiseRoundOverIfReady(eventProvider);
        }

        [RconEvent("server.onRoundOverPlayers")]
        private void RoundOverPlayersEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            IPlayerCollection players = new PlayerInfoBlockPlayerCollection(data);
            RoundOverState state = RoundOverData[eventProvider];

            state.Players = players;

            RaiseRoundOverIfReady(eventProvider);
        }

        [RconEvent("server.onRoundOverTeamScores")]
        private void RoundOverTeamScoresEventHandler(IRconEventProvider eventProvider, IList<string> data)
        {
            int numTeamScores = data[0].ToInt();
            List<int> teamScores = new List<int>(numTeamScores);
            int targetScore;
            RoundOverState state = RoundOverData[eventProvider];

            for (int i = 0; i < numTeamScores; i++)
            {
                teamScores.Add(data[i + 1].ToInt());
            }

            targetScore = data[numTeamScores + 1].ToInt();

            state.TeamScores = new ReadOnlyCollection<int>(teamScores);
            state.TargetScore = targetScore;

            RaiseRoundOverIfReady(eventProvider);
        }

        private void RaiseRoundOverIfReady(IRconEventProvider eventProvider)
        {
            RoundOverEventArgs args;
            RoundOverState state = RoundOverData[eventProvider];

            if (!state.HasAllData) return;

            args = new RoundOverEventArgs(state.TeamId, state.Players, state.TeamScores, state.TargetScore);

            state.Reset();

            RaiseEvent(eventProvider, RoundOver, args);
        }
        #endregion

        private void RconEventHandler(object sender, RconEventArgs e)
        {
            Action<IRconEventProvider, IList<string>> eventHandler;

            if (!EventHandlers.TryGetValue(e.EventName, out eventHandler)) return;

            eventHandler((IRconEventProvider)sender, e.EventData);
        }

        private Player GetPlayer(IRconEventProvider eventProvider, string name, params string[] properties)
        {
            ISynchronousRconClient client;
            Player result = null;
            PlayerCollection players;

            if (Clients.TryGetValue(eventProvider, out client))
            {
                players = new PlayerCollection(client);

                result = players.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
            }

            if (result == null)
            {
                result = CreatePlayer(name, properties);
            }

            return result;
        }

        private Player CreatePlayer(string name, string[] properties)
        {
            IDictionary<string, string> propertyDict = CreateDictionary(properties);
            RconPlayer player = new RconPlayer(propertyDict);

            return player;
        }

        private IDictionary<string, string> CreateDictionary(string[] properties)
        {
            if (properties.Length % 2 != 0)
                throw new ArgumentException("Property array must be of even length", "properties");

            int numProperties = properties.Length / 2;
            string key;
            string value;
            Dictionary<string, string> result = new Dictionary<string, string>(numProperties);

            for (int i = 0; i < numProperties; i++)
            {
                key = properties[2 * i];
                value = properties[2 * i + 1];

                result[key] = value;
            }

            return result;
        }

        private static void RaiseEvent<TEventArgs>(IRconEventProvider eventProvider, EventHandler<TEventArgs> eventDelegate, TEventArgs args) where TEventArgs : EventArgs
        {
            if (eventDelegate != null)
                eventDelegate(eventProvider, args);
        }



        #region Event Handler Helpers
        [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
        private sealed class RconEventAttribute : Attribute
        {
            public string EventName { get; private set; }

            public RconEventAttribute(string eventName)
            {
                EventName = eventName;
            }
        }

        private void CreateEventHandlers()
        {
            MethodInfo[] methods = typeof(RconEventManager).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            RconEventAttribute attrib;
            Action<IRconEventProvider, IList<string>> eventHandler;

            foreach (MethodInfo m in methods)
            {
                attrib = GetRconEventAttribute(m);

                if (attrib == null) continue;

                eventHandler = (Action<IRconEventProvider, IList<string>>)Delegate.CreateDelegate(
                    typeof(Action<IRconEventProvider, IList<string>>),
                    this,
                    m);

                EventHandlers[attrib.EventName] = eventHandler;
            }
        }

        private static RconEventAttribute GetRconEventAttribute(MethodInfo methodInfo)
        {
            object[] attribs = methodInfo.GetCustomAttributes(typeof(RconEventAttribute), false);

            if (attribs.Length == 0)
                return null;

            return (RconEventAttribute)attribs[0];
        }
        #endregion

        private PlayerSubset ParsePlayerSubset(IRconEventProvider eventProvider, IList<string> data, int offset)
        {
            PlayerSubset result;
            PlayerSubsetType type = ParseType(data, offset);
            int team;
            int squad;
            Player player;

            switch (type)
            {
                case PlayerSubsetType.All:
                    result = PlayerSubset.CreateAll();
                    break;

                case PlayerSubsetType.Team:
                    team = int.Parse(data[offset + 1]);
                    result = PlayerSubset.CreateTeam(team);
                    break;

                case PlayerSubsetType.Squad:
                    team = int.Parse(data[offset + 1]);
                    squad = int.Parse(data[offset + 2]);
                    result = PlayerSubset.CreateSquad(team, squad);
                    break;

                case PlayerSubsetType.Player:
                    player = GetPlayer(eventProvider, data[offset + 1]);
                    result = PlayerSubset.CreatePlayer(player);
                    break;

                case PlayerSubsetType.Unknown:
                default:
                    result = PlayerSubset.CreateUnknown();
                    break;
            }

            return result;
        }

        private static PlayerSubsetType ParseType(IList<string> data, int offset)
        {
            PlayerSubsetType result;

            if (!Enum.TryParse(data[offset], true, out result))
                return PlayerSubsetType.Unknown;

            return result;
        }
    }
}
